package com.w.picture.manager.websocket;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.w.picture.manager.disruptor.PictureEditEventProducer;
import com.w.picture.mode.entity.User;
import com.w.picture.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.Resource;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 图片编辑处理器(WebSocket 处理器类),在连接成功,连接关闭,接收到客户端消息时,会回调对应的方法
 * TextWebSocketHandler 是一个抽象类,支持以字符串的方式发送和接收消息,不支持二进制
 *
 * @author wzp
 * @since 2025/2/2
 */
@Slf4j
@Component
public class PictureEditHandler extends TextWebSocketHandler {

    @Resource
    private UserService userService;
    @Resource
    private PictureEditEventProducer pictureEditEventProducer;

    // 注意: 由于可能同时存在多个WebSocket客户端建立连接和发送消息,所以使用JUC 的 ConcurrentHashMap 来存储数据
    // 每张图片的编辑状态: key = pictureId, value = userId 当前正在编辑的用户 id
    private final Map<Long, Long> pictureEditingUsers = new ConcurrentHashMap<>();
    // 保存所有连接的会话: key = userId, value = WebSocketSession 用户会话集合
    private final Map<Long, Set<WebSocketSession>> pictureSessions = new ConcurrentHashMap<>();


    /**
     * 当WebSocket连接成功时，会回调此方法。
     * 这段代码定义了WebSocket连接建立后的处理逻辑，具体是将用户的会话保存到集合中，并向与特定图片ID关联的所有用户广播一条通知消息，告知有新用户加入编辑。
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 从当前WebSocketSession的属性中获取User对象和pictureId。
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");

        // 使用putIfAbsent确保每个pictureId只创建一次对应的ConcurrentHashSet。
        // 这样可以安全地并发访问这些集合，不用担心线程安全问题。
        // ConcurrentHashMap.newKeySet() 是 Java 8 引入的一种便捷方法，用于创建一个具有与 ConcurrentHashMap 相同并发性能的 Set 集合。这个方法返回一个 Set 视图，其实现基于 ConcurrentHashMap，这意味着它提供了与 ConcurrentHashMap 类似的线程安全保证和并发性能。
        // 当你需要一个线程安全的集合来存储唯一元素，并且希望在高并发环境下仍能保持良好的性能时，ConcurrentHashMap.newKeySet() 就非常有用。例如，在 WebSocket 应用中，如果你需要维护每个用户会话的集合，并确保这些集合能在多线程环境中正确地读取和写入，就可以使用这种方法。
        pictureSessions.putIfAbsent(pictureId, ConcurrentHashMap.newKeySet());

        // 将当前用户的WebSocketSession添加到对应pictureId的会话集合中。
        pictureSessions.get(pictureId).add(session);

        // 构造一个响应消息，用于通知其他用户当前用户已加入编辑。
        PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
        // 设置消息类型为INFO。
        pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());

        // 创建一条包含用户名的消息，表示该用户加入了编辑。
        String message = String.format("%s加入编辑", user.getUserName());
        pictureEditResponseMessage.setMessage(message);

        // 获取并设置用户详情信息到响应消息中。
        pictureEditResponseMessage.setUser(userService.getUserVo(user));

        // 调用broadcastToPicture方法，向所有正在编辑同一张图片的其他用户广播这条消息。
        broadcastToPicture(pictureId, pictureEditResponseMessage, session);
    }

    /**
     * 当WebSocket收到客户端消息时，会回调此方法。
     * 这段代码定义了一个WebSocket消息处理器方法 handleTextMessage，它负责处理从客户端接收到的文本消息。根据消息类型的不同，它将调用不同的处理逻辑，并在遇到未知消息类型时返回错误响应。
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 将接收到的JSON格式的消息字符串解析为PictureEditRequestMessage对象。
        PictureEditRequestMessage pictureEditRequestMessage = JSONUtil.toBean(message.getPayload(), PictureEditRequestMessage.class);

        // 从解析后的消息对象中获取消息类型。
        String type = pictureEditRequestMessage.getType();
        PictureEditMessageTypeEnum pictureEditMessageTypeEnum = PictureEditMessageTypeEnum.valueOf(type);

        // 从当前WebSocketSession的属性中获取User对象和pictureId。
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");

        // 注释下面的switch代码,修改为:将消息传递给PictureEditEventProducer进行事件处理
        // 这样,就实现了基于 Disruptor 的异步消息处理机制,将原有的同步消息分发逻辑改造为高效解耦的异步处理模型,也更有利于代码的扩展
        pictureEditEventProducer.publishEvent(pictureEditRequestMessage, session, user, pictureId);

        // 根据消息类型，使用switch-case结构选择相应的处理方法。
        // 注意: 这里的处理逻辑是可以单独封装成一个类，以实现更好的代码复用和可维护性。也就是策略模式
//        switch (pictureEditMessageTypeEnum) {
//            case ENTER_EDIT:
//                // 处理用户进入编辑的消息
//                handleEnterEditMessage(pictureEditRequestMessage, session, user, pictureId);
//                break;
//            case EDIT_ACTION:
//                // 处理用户的编辑操作消息
//                handleEditActionMessage(pictureEditRequestMessage, session, user, pictureId);
//                break;
//            case EXIT_EDIT:
//                // 处理用户退出编辑的消息
//                handleExitEditMessage(pictureEditRequestMessage, session, user, pictureId);
//                break;
//            default:
//                // 如果消息类型不在预期内，则构建一个错误响应消息并发送回客户端。
//                PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
//                pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ERROR.getValue());
//                pictureEditResponseMessage.setMessage("未知消息类型");
//                pictureEditResponseMessage.setUser(userService.getUserVo(user));
//
//                // 将错误响应消息序列化为JSON字符串，并通过WebSocket会话发送给客户端。
//                session.sendMessage(new TextMessage(JSONUtil.toJsonStr(pictureEditResponseMessage)));
//        }
    }

    /**
     * 当WebSocket连接关闭时调用。
     * 此方法负责清理用户编辑状态、移除会话，并广播用户离开的消息。
     *
     * @param session WebSocket会话对象
     * @param closeStatus 关闭状态信息
     * @throws Exception 抛出异常表示处理过程中出现了错误
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        // 获取当前WebSocketSession的属性
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");

        // 移除当前用户的编辑状态
        handleExitEditMessage(null, session, user, pictureId);

        // 删除会话
        Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
        if (sessionSet != null) {
            sessionSet.remove(session);
            if (sessionSet.isEmpty()) {
                pictureSessions.remove(pictureId);
            }
        }

        // 响应断开连接的消息
        PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
        pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
        String message = String.format("%s离开编辑", user.getUserName());
        pictureEditResponseMessage.setMessage(message);
        pictureEditResponseMessage.setUser(userService.getUserVo(user));
        broadcastToPicture(pictureId, pictureEditResponseMessage);
    }

    /**
     * 广播图片编辑消息:用于向与特定图片ID关联的所有WebSocket会话广播消息,但是不排除任何会话
     *
     * @param pictureId
     * @param pictureEditResponseMessage
     * @throws Exception
     */
    private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage) throws Exception {
        broadcastToPicture(pictureId, pictureEditResponseMessage, null);
    }


    /**
     * 广播图片编辑消息:用于向与特定图片ID关联的所有WebSocket会话广播消息，但排除指定的会话
     * <p>
     * JSON序列化配置讨论
     * 在这个实现中有两种方式来确保长整型（Long）不会在序列化为JSON时失去精度：
     * <p>
     * 直接配置ObjectMapper：使用WRITE_NUMBERS_AS_STRINGS特性，强制将所有数值类型作为字符串输出。这种方法适用于希望统一处理所有数值类型的场景。
     * 注册自定义模块并添加序列化器：针对具体的类型（如Long和long）注册ToStringSerializer。这种方法提供了更细粒度的控制，只影响特定类型的序列化过程。
     * 哪种方法更好？
     * <p>
     * 如果你的应用仅需处理长整型的精度问题，那么第二种方法（注册自定义模块并添加序列化器）更为合适。因为它只影响你关心的数据类型，而不会影响其他数值类型。
     * 第一种方法虽然简单，但它会对所有数值类型生效，这可能不是你想要的结果，特别是当你有其他不需要这样处理的数值类型时。
     * 因此，在大多数情况下，推荐使用第二种方法，它提供了更好的灵活性和精确控制。不过，具体选择应该基于项目需求和实际情况。
     *
     * @param pictureId
     * @param pictureEditResponseMessage
     * @param excludesSession
     * @throws Exception
     */
    private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage, WebSocketSession excludesSession) throws Exception {
        // 从pictureSessions中获取与pictureId关联的所有WebSocket会话集合
        Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);

        // 检查sessionSet是否非空（即是否存在与该图片ID相关的WebSocket会话）
        if (CollUtil.isNotEmpty(sessionSet)) {
            // 创建ObjectMapper实例，这是Jackson库的核心类之一，用于处理JSON序列化和反序列化
            ObjectMapper objectMapper = new ObjectMapper();

            // 配置objectMapper将数字写为字符串形式，以避免JavaScript端处理长整型数字时可能发生的精度丢失问题
//            objectMapper.configure(com.fasterxml.jackson.core.JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true);

            // 创建SimpleModule实例，并添加自定义序列化器，将Long类型转换为字符串表示形式
            SimpleModule module = new SimpleModule();
            module.addSerializer(Long.class, ToStringSerializer.instance); // 对Long对象类型进行序列化
            module.addSerializer(Long.TYPE, ToStringSerializer.instance); // 对long基本数据类型也进行序列化

            // 将模块注册到objectMapper中
            objectMapper.registerModule(module);

            // 使用objectMapper将pictureEditResponseMessage对象序列化为JSON格式的字符串
            String message = objectMapper.writeValueAsString(pictureEditResponseMessage);

            // 创建一个TextMessage对象，准备通过WebSocket发送
            TextMessage textMessage = new TextMessage(message);

            // 遍历所有与pictureId相关的WebSocket会话
            for (WebSocketSession session : sessionSet) {
                // 如果当前会话是需要被排除的会话，则跳过发送消息给这个会话
                if (excludesSession != null && excludesSession.equals(session)) {
                    continue;
                }

                // 检查会话是否仍然处于打开状态，如果是，则发送消息
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                }
            }
        }
    }

    /**
     * 处理进入编辑消息
     * 首先是用户进入编辑状态,要设置当前用户为编辑用户,并且向其他客户端发送消息，通知其他用户该图片正在被当前用户编辑。
     *
     * @param pictureEditRequestMessage 包含请求信息的消息对象
     * @param session                   WebSocket会话对象
     * @param user                      当前操作的用户对象
     * @param pictureId                 当前正在编辑的图片ID
     * @throws Exception 抛出异常表示处理过程中出现了错误
     */
    public void handleEnterEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws Exception {
        // 使用同步块确保“检查-然后-执行”的原子性。
        // 即使pictureEditingUsers是线程安全的ConcurrentHashMap，单独的containsKey和put方法调用并不是原子性的，
        // 因此我们需要一个同步机制来防止多个线程同时检查到相同的pictureId不存在并尝试添加它，导致竞态条件。
        synchronized (pictureEditingUsers) { // 确保线程安全
            if (!pictureEditingUsers.containsKey(pictureId)) {
                // 设置当前用户为编辑用户，并保存到集合中
                pictureEditingUsers.put(pictureId, user.getId());

                // 构造响应消息
                PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
                pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
                String message = String.format("%s开始编辑图片", user.getUserName());
                pictureEditResponseMessage.setMessage(message);
                pictureEditResponseMessage.setUser(userService.getUserVo(user));

                // 广播给同一张图片的其他用户
                broadcastToPicture(pictureId, pictureEditResponseMessage);

                log.info("{} 开始编辑图片 ID: {}", user.getUserName(), pictureId);
            } else {
                // 如果已经有其他用户正在编辑该图片，则返回失败信息给当前用户
                PictureEditResponseMessage responseMessage = new PictureEditResponseMessage();
                responseMessage.setType(PictureEditMessageTypeEnum.ERROR.getValue());
                responseMessage.setMessage("该图片正在被其他用户编辑");
                responseMessage.setUser(userService.getUserVo(user));

                session.sendMessage(new TextMessage(JSONUtil.toJsonStr(responseMessage)));

                log.warn("{} 尝试编辑已被占用的图片 ID: {}", user.getUserName(), pictureId);
            }
        }
    }

    /**
     * 处理用户的编辑操作消息。
     * 这段代码定义了一个名为 handleEditActionMessage 的方法，用于处理用户执行的编辑操作。它首先检查发起请求的用户是否是当前正在编辑该图片的用户，然后根据检查结果构建并广播相应的消息给其他用户（除了发起请求的用户）。
     *
     * @param pictureEditRequestMessage 包含请求信息的消息对象
     * @param session                   WebSocket会话对象
     * @param user                      当前操作的用户对象
     * @param pictureId                 当前正在编辑的图片ID
     * @throws Exception 抛出异常表示处理过程中出现了错误
     */
    public void handleEditActionMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws Exception {
        // 从pictureEditingUsers中获取正在编辑该图片的用户ID
        Long editingUserId = pictureEditingUsers.get(pictureId);

        // 获取编辑动作
        String editAction = pictureEditRequestMessage.getEditAction();
        PictureEditActionEnum actionEnum = PictureEditActionEnum.getEnumByValue(editAction);

        // 如果动作无效，则直接返回
        if (actionEnum == null) {
            return;
        }

        // 确认发起请求的用户是当前编辑者
        if (editingUserId != null && editingUserId.equals(user.getId())) {
            // 构造响应消息
            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EDIT_ACTION.getValue());
            String message = String.format("%s执行%s", user.getUserName(), actionEnum.getText());
            pictureEditResponseMessage.setMessage(message);
            pictureEditResponseMessage.setEditAction(editAction);
            pictureEditResponseMessage.setUser(userService.getUserVo(user));

            // 广播给除了当前客户端之外的其他用户，否则会造成重复编辑
            broadcastToPicture(pictureId, pictureEditResponseMessage, session);

            log.info("{} 执行了编辑动作 {} 在图片 ID: {}", user.getUserName(), editAction, pictureId);
        } else {
            // 如果当前用户不是编辑者，则记录警告日志
            log.warn("{} 尝试执行编辑动作 {}，但不是当前编辑者 图片 ID: {}", user.getUserName(), editAction, pictureId);

            // 可以选择发送一个错误响应给用户
            PictureEditResponseMessage errorResponse = new PictureEditResponseMessage();
            errorResponse.setType(PictureEditMessageTypeEnum.ERROR.getValue());
            errorResponse.setMessage("您不是当前编辑者");
            errorResponse.setUser(userService.getUserVo(user));
            session.sendMessage(new TextMessage(JSONUtil.toJsonStr(errorResponse)));
        }
    }

    /**
     * 处理用户退出编辑的请求。
     * 这段代码定义了一个名为 handleExitEditMessage 的方法，用于处理用户退出编辑的请求。它首先检查发起请求的用户是否是当前正在编辑该图片的用户，然后根据检查结果移除用户的编辑状态，并广播相应的退出消息给其他用户。
     * 使用 computeIfPresent 方法确保“检查-然后-执行”操作的原子性，避免竞态条件。
     * 虽然 containsKey 和 remove 方法本身都是线程安全的，但是这两个操作组合在一起（即“检查-然后-执行”模式）并不是原子性的。
     * 这意味着在多线程环境下，可能会发生竞态条件（race condition），例如：
     * <p>
     * 线程A检查到 pictureId 对应的编辑者确实是当前用户。
     * 在线程A调用 remove 之前，线程B也检查了 pictureId 并发现它对应的编辑者是同一个用户。
     * 线程A和线程B都尝试移除它们的用户ID，导致潜在的数据不一致或其他未定义行为。
     *
     * @param pictureEditRequestMessage 包含请求信息的消息对象
     * @param session                   WebSocket会话对象
     * @param user                      当前操作的用户对象
     * @param pictureId                 当前正在编辑的图片ID
     * @throws Exception 抛出异常表示处理过程中出现了错误
     */
    public void handleExitEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws Exception {
        // 使用 computeIfPresent 确保“检查-然后-执行”的原子性
        pictureEditingUsers.computeIfPresent(pictureId, (key, userId) -> {
            // 检查当前用户是否是该图片的编辑者
            if (userId.equals(user.getId())) {
                try {
                    // 构造响应消息
                    PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
                    pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EXIT_EDIT.getValue());
                    String message = String.format("%s退出编辑图片", user.getUserName());
                    pictureEditResponseMessage.setMessage(message);
                    pictureEditResponseMessage.setUser(userService.getUserVo(user));

                    // 广播给所有相关用户（除了当前客户端）
                    broadcastToPicture(pictureId, pictureEditResponseMessage, session);

                    log.info("{} 退出了编辑 图片 ID: {}", user.getUserName(), pictureId);
                } catch (Exception e) {
                    log.error("Failed to broadcast exit edit message for picture ID: {}", pictureId, e);
                }

                // 返回 null 表示移除该键值对 computeIfPresent 方法 本身的操作这是
                return null;
            }
            // 如果不是当前用户，则保持原来的值，不作任何修改
            return userId;
        });

        // 退出操作只需要移除即可,不然点击退出后会得到您不是当前编辑者异常消息.
        // 如果 computeIfPresent 没有成功移除键值对（即返回 false），则说明当前用户不是编辑者或编辑状态已变更
//        if (!pictureEditingUsers.containsKey(pictureId)) {
//            log.warn("{} 尝试退出编辑，但不是当前编辑者或编辑者已变更 图片 ID: {}", user.getUserName(), pictureId);
//
//            // 可以选择发送一个错误响应给用户
//            PictureEditResponseMessage errorResponse = new PictureEditResponseMessage();
//            errorResponse.setType(PictureEditMessageTypeEnum.ERROR.getValue());
//            errorResponse.setMessage("您不是当前编辑者");
//            errorResponse.setUser(userService.getUserVo(user));
//            session.sendMessage(new TextMessage(JSONUtil.toJsonStr(errorResponse)));
//        }
    }


}
